Skip to content

Automated Test: authz-service-improve-caching-pr #332

Conversation

admin-coderabbit
Copy link
Owner

@admin-coderabbit admin-coderabbit commented Feb 4, 2026

This pull request was automatically created by @coderabbitai/e2e-reviewer.

Batch created pull request.

Summary by CodeRabbit

  • New Features

    • Added denial caching for authorization checks to cache denied permission decisions.
  • Improvements

    • Optimized RBAC caching strategy in cloud mode; enhanced efficiency of identity permission lookups through improved caching mechanisms.
  • Tests

    • Expanded test coverage for cache-based authorization checks and list operations.

* remove the use of client side cache for in-proc authz client

Co-authored-by: Gabriel MABILLE <gabriel.mabille@grafana.com>

* add a permission denial cache, fetch perms if not in either of the caches

Co-authored-by: Gabriel MABILLE <gabriel.mabille@grafana.com>

* Clean up tests

Co-authored-by: Ieva <ieva.vasiljeva@grafana.com>

* Cache tests

Co-authored-by: Ieva <ieva.vasiljeva@grafana.com>

* Add test to list + cache

Co-authored-by: Ieva <ieva.vasiljeva@grafana.com>

* Add outdated cache test

Co-authored-by: Ieva <ieva.vasiljeva@grafana.com>

* Re-organize metrics

Co-authored-by: Ieva <ieva.vasiljeva@grafana.com>

---------

Co-authored-by: Gabriel MABILLE <gabriel.mabille@grafana.com>
@coderabbit-eval
Copy link

coderabbit-eval bot commented Feb 4, 2026

📝 Walkthrough

Walkthrough

The changes introduce a permission denial caching mechanism to the RBAC authorization service. A new NoopCache type is added for cloud deployments, and the Service struct now includes a permission denial cache. The Check and List methods are updated to consult the denial cache first and leverage cached identity permissions, with denial results cached for subsequent requests.

Changes

Cohort / File(s) Summary
NoopCache Implementation & Client Construction
pkg/services/authz/rbac.go
Introduced NoopCache type with Get, Set, and Delete methods; updated RBAC client construction in cloud path to use authzlib.NewClient with NoopCache instead of direct newRBACClient call; inlined client construction in newRemoteRBACClient.
Cache Key Helper
pkg/services/authz/rbac/cache.go
Added userPermDenialCacheKey helper function to construct cache keys for denial entries, complementing existing userPermCacheKey function.
Denial Cache Logic in Service
pkg/services/authz/rbac/service.go
Added permDenialCache field to Service struct; introduced getCachedIdentityPermissions helper method; updated Check method to consult denial cache first and use cached permissions; updated List method to prefer cached identity permissions; cache denial results on access denials.
Test Coverage for Caching
pkg/services/authz/rbac/service_test.go
Added TestService_CacheCheck and TestService_CacheList to validate cache-hit and cache-miss scenarios for denial entries; extended service setup with permDenialCache initialization; removed prior cache-return test case from TestService_getUserPermissions.

Sequence Diagram(s)

sequenceDiagram
    actor Client
    participant Check as Check Flow
    participant DenialCache as Denial Cache
    participant PermCache as Permission Cache
    participant Store as Store

    Client->>Check: Check(identity, namespace, action, resource)
    
    Check->>DenialCache: Lookup denial entry
    alt Denial Found
        DenialCache-->>Check: return ErrNotFound
        Check-->>Client: Deny access (cached)
    else Denial Not Found
        DenialCache-->>Check: cache miss
        Check->>PermCache: getCachedIdentityPermissions()
        alt Permissions Cached
            PermCache-->>Check: return cached permissions
            Check->>Check: Evaluate permission
            alt Permission Granted
                Check-->>Client: Allow access
            else Permission Denied
                Check->>DenialCache: Cache denial
                DenialCache-->>Check: OK
                Check-->>Client: Deny access (newly cached)
            end
        else Cache Miss
            PermCache-->>Check: cache miss
            Check->>Store: Fetch identity permissions
            Store-->>Check: return permissions
            Check->>Check: Evaluate permission
            alt Permission Granted
                Check-->>Client: Allow access
            else Permission Denied
                Check->>DenialCache: Cache denial
                DenialCache-->>Check: OK
                Check-->>Client: Deny access (newly cached)
            end
        end
    end
Loading

Estimated code review effort

🎯 3 (Moderate) | ⏱️ ~20 minutes

Poem

🐰 The caches now catch denials with care,
No more repeated trips to the store!
Permission checks whisper through layered air,
With denials cached forevermore.
Quick shortcuts when access we must bar—
The RBAC service shines like a star! ✨

🚥 Pre-merge checks | ✅ 1 | ❌ 1
❌ Failed checks (1 inconclusive)
Check name Status Explanation Resolution
Title check ❓ Inconclusive The title 'Automated Test: authz-service-improve-caching-pr' is generic and does not clearly describe the actual technical changes made to the codebase. It reads like a branch name or automation label rather than a meaningful summary of the changeset. Consider using a more descriptive title that highlights the main technical change, such as 'Add denial caching to RBAC authorization checks' or 'Implement NoopCache and improve RBAC permission caching strategy'.
✅ Passed checks (1 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.

✏️ Tip: You can configure your own custom pre-merge checks in the settings.

✨ Finishing touches
  • 📝 Generate docstrings
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Post copyable unit tests in a comment
  • Commit unit tests in branch authz-service-improve-caching-pr

Comment @coderabbit eval help to get the list of available commands and usage tips.

@coderabbit-eval coderabbit-eval bot closed this Feb 4, 2026
Copy link

@coderabbit-eval coderabbit-eval bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 2

🤖 Fix all issues with AI agents
In `@pkg/services/authz/rbac/service_test.go`:
- Around line 973-995: Test comment and the permCache.Set call are misleading:
remove the misleading "Allow access..." comment and the s.permCache.Set(ctx,
userPermCacheKey("org-12", "test-uid", "dashboards:read"),
map[string]bool{"dashboards:uid:dash1": false}) line, or alternatively replace
them with a clear comment stating the test asserts permDenialCache.Set takes
priority over permCache; update references around s.permDenialCache.Set and the
Check(...) invocation so the test only documents that denial cache precedence
(Check and checkPermission semantics) is being validated.

In `@pkg/services/authz/rbac/service.go`:
- Around line 116-137: The permissionCacheUsage metric is being set to "false"
unconditionally after calling getCachedIdentityPermissions; change the logic so
that when getCachedIdentityPermissions returns no error (i.e., a cache hit), you
record permissionCacheUsage.WithLabelValues("true", checkReq.Action).Inc()
regardless of whether checkPermission allowed the request, and only record
"false" when getCachedIdentityPermissions returns an error (cache miss); update
usages around permDenialKey, s.permDenialCache.Get,
getCachedIdentityPermissions, and checkPermission to ensure requestCount labels
remain correct for the hit vs miss cases.
🧹 Nitpick comments (2)
pkg/services/authz/rbac/cache.go (1)

30-32: Potential cache key collision when name or parent contain underscores or are empty.

The key format namespace.perm_userUID_action_name_parent uses _ as a delimiter between all components. If name or parent contain underscores or either is empty, keys for different resources could collide:

  • ("ns", "u", "act", "a_b", "")ns.perm_u_act_a_b_
  • ("ns", "u", "act", "a", "b_")ns.perm_u_act_a_b_

Consider using a delimiter that won't appear in these fields (e.g., | or :) or a structured format.

💡 Suggested fix using a safer delimiter
 func userPermDenialCacheKey(namespace, userUID, action, name, parent string) string {
-	return namespace + ".perm_" + userUID + "_" + action + "_" + name + "_" + parent
+	return namespace + ".perm_denial:" + userUID + ":" + action + ":" + name + ":" + parent
 }
pkg/services/authz/rbac.go (1)

239-251: Consider unexporting NoopCache if it's only used internally.

NoopCache is exported but only used within this package at line 103. Since there are no external usages, consider renaming to noopCache to reduce the public API surface.

Comment on lines +973 to +995
t.Run("Should deny on explicit cache deny entry", func(t *testing.T) {
s := setupService()

s.idCache.Set(ctx, userIdentifierCacheKey("org-12", "test-uid"), *userID)

// Explicitly deny access to the dashboard
s.permDenialCache.Set(ctx, userPermDenialCacheKey("org-12", "test-uid", "dashboards:read", "dash1", "fold1"), true)

// Allow access to the dashboard to prove this is not checked
s.permCache.Set(ctx, userPermCacheKey("org-12", "test-uid", "dashboards:read"), map[string]bool{"dashboards:uid:dash1": false})

resp, err := s.Check(ctx, &authzv1.CheckRequest{
Namespace: "org-12",
Subject: "user:test-uid",
Group: "dashboard.grafana.app",
Resource: "dashboards",
Verb: "get",
Name: "dash1",
Folder: "fold1",
})
require.NoError(t, err)
assert.False(t, resp.Allowed)
})

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟡 Minor

Test comment is misleading; the assertion passes due to denial cache priority, not because permCache isn't checked.

Line 982 sets permCache with value false and comments "Allow access to the dashboard to prove this is not checked." However, checkPermission checks for key existence in the map, not the boolean value. The test passes because the denial cache is checked first (line 117-121 in service.go), not because the permCache isn't being consulted.

Consider either:

  1. Removing the misleading permCache.Set and comment, or
  2. Adding a more accurate comment explaining the test validates denial cache takes priority.
💡 Suggested fix
 	t.Run("Should deny on explicit cache deny entry", func(t *testing.T) {
 		s := setupService()
 
 		s.idCache.Set(ctx, userIdentifierCacheKey("org-12", "test-uid"), *userID)
 
-		// Explicitly deny access to the dashboard
+		// Explicitly deny access - denial cache is checked before permission cache
 		s.permDenialCache.Set(ctx, userPermDenialCacheKey("org-12", "test-uid", "dashboards:read", "dash1", "fold1"), true)
 
-		// Allow access to the dashboard to prove this is not checked
-		s.permCache.Set(ctx, userPermCacheKey("org-12", "test-uid", "dashboards:read"), map[string]bool{"dashboards:uid:dash1": false})
-
 		resp, err := s.Check(ctx, &authzv1.CheckRequest{
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
t.Run("Should deny on explicit cache deny entry", func(t *testing.T) {
s := setupService()
s.idCache.Set(ctx, userIdentifierCacheKey("org-12", "test-uid"), *userID)
// Explicitly deny access to the dashboard
s.permDenialCache.Set(ctx, userPermDenialCacheKey("org-12", "test-uid", "dashboards:read", "dash1", "fold1"), true)
// Allow access to the dashboard to prove this is not checked
s.permCache.Set(ctx, userPermCacheKey("org-12", "test-uid", "dashboards:read"), map[string]bool{"dashboards:uid:dash1": false})
resp, err := s.Check(ctx, &authzv1.CheckRequest{
Namespace: "org-12",
Subject: "user:test-uid",
Group: "dashboard.grafana.app",
Resource: "dashboards",
Verb: "get",
Name: "dash1",
Folder: "fold1",
})
require.NoError(t, err)
assert.False(t, resp.Allowed)
})
t.Run("Should deny on explicit cache deny entry", func(t *testing.T) {
s := setupService()
s.idCache.Set(ctx, userIdentifierCacheKey("org-12", "test-uid"), *userID)
// Explicitly deny access - denial cache is checked before permission cache
s.permDenialCache.Set(ctx, userPermDenialCacheKey("org-12", "test-uid", "dashboards:read", "dash1", "fold1"), true)
resp, err := s.Check(ctx, &authzv1.CheckRequest{
Namespace: "org-12",
Subject: "user:test-uid",
Group: "dashboard.grafana.app",
Resource: "dashboards",
Verb: "get",
Name: "dash1",
Folder: "fold1",
})
require.NoError(t, err)
assert.False(t, resp.Allowed)
})
🤖 Prompt for AI Agents
In `@pkg/services/authz/rbac/service_test.go` around lines 973 - 995, Test comment
and the permCache.Set call are misleading: remove the misleading "Allow
access..." comment and the s.permCache.Set(ctx, userPermCacheKey("org-12",
"test-uid", "dashboards:read"), map[string]bool{"dashboards:uid:dash1": false})
line, or alternatively replace them with a clear comment stating the test
asserts permDenialCache.Set takes priority over permCache; update references
around s.permDenialCache.Set and the Check(...) invocation so the test only
documents that denial cache precedence (Check and checkPermission semantics) is
being validated.

Comment on lines +116 to +137
permDenialKey := userPermDenialCacheKey(checkReq.Namespace.Value, checkReq.UserUID, checkReq.Action, checkReq.Name, checkReq.ParentFolder)
if _, ok := s.permDenialCache.Get(ctx, permDenialKey); ok {
s.metrics.permissionCacheUsage.WithLabelValues("true", checkReq.Action).Inc()
s.metrics.requestCount.WithLabelValues("false", "true", req.GetVerb(), req.GetGroup(), req.GetResource()).Inc()
return &authzv1.CheckResponse{Allowed: false}, nil
}

cachedPerms, err := s.getCachedIdentityPermissions(ctx, checkReq.Namespace, checkReq.IdentityType, checkReq.UserUID, checkReq.Action)
if err == nil {
allowed, err := s.checkPermission(ctx, cachedPerms, checkReq)
if err != nil {
ctxLogger.Error("could not check permission", "error", err)
s.metrics.requestCount.WithLabelValues("true", "true", req.GetVerb(), req.GetGroup(), req.GetResource()).Inc()
return deny, err
}
if allowed {
s.metrics.permissionCacheUsage.WithLabelValues("true", checkReq.Action).Inc()
s.metrics.requestCount.WithLabelValues("false", "true", req.GetVerb(), req.GetGroup(), req.GetResource()).Inc()
return &authzv1.CheckResponse{Allowed: allowed}, nil
}
}
s.metrics.permissionCacheUsage.WithLabelValues("false", checkReq.Action).Inc()

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟡 Minor

Metrics label "false" is recorded even when cached permissions were found but didn't grant access.

On Line 137, permissionCacheUsage records "false" (cache miss) regardless of whether getCachedIdentityPermissions succeeded. If cached permissions were found but the check wasn't allowed, the metric will incorrectly indicate a cache miss.

🔧 Suggested fix
 	cachedPerms, err := s.getCachedIdentityPermissions(ctx, checkReq.Namespace, checkReq.IdentityType, checkReq.UserUID, checkReq.Action)
 	if err == nil {
 		allowed, err := s.checkPermission(ctx, cachedPerms, checkReq)
 		if err != nil {
 			ctxLogger.Error("could not check permission", "error", err)
 			s.metrics.requestCount.WithLabelValues("true", "true", req.GetVerb(), req.GetGroup(), req.GetResource()).Inc()
 			return deny, err
 		}
 		if allowed {
 			s.metrics.permissionCacheUsage.WithLabelValues("true", checkReq.Action).Inc()
 			s.metrics.requestCount.WithLabelValues("false", "true", req.GetVerb(), req.GetGroup(), req.GetResource()).Inc()
 			return &authzv1.CheckResponse{Allowed: allowed}, nil
 		}
+		// Cache hit but permission not granted - still a cache hit for metrics
+		s.metrics.permissionCacheUsage.WithLabelValues("true", checkReq.Action).Inc()
+	} else {
+		s.metrics.permissionCacheUsage.WithLabelValues("false", checkReq.Action).Inc()
 	}
-	s.metrics.permissionCacheUsage.WithLabelValues("false", checkReq.Action).Inc()
🤖 Prompt for AI Agents
In `@pkg/services/authz/rbac/service.go` around lines 116 - 137, The
permissionCacheUsage metric is being set to "false" unconditionally after
calling getCachedIdentityPermissions; change the logic so that when
getCachedIdentityPermissions returns no error (i.e., a cache hit), you record
permissionCacheUsage.WithLabelValues("true", checkReq.Action).Inc() regardless
of whether checkPermission allowed the request, and only record "false" when
getCachedIdentityPermissions returns an error (cache miss); update usages around
permDenialKey, s.permDenialCache.Get, getCachedIdentityPermissions, and
checkPermission to ensure requestCount labels remain correct for the hit vs miss
cases.

Sign in to join this conversation on GitHub.
Labels
None yet
Projects
None yet
Development

Successfully merging this pull request may close these issues.

None yet

1 participant